fix(elementor): port v1.9.0 data layer — save_page_data wp_slash fallback, CSS invalidation, by-ref insert/remove/update, normalize_container_settings, generate_id uses random_bytes
This commit is contained in:
@@ -3,319 +3,287 @@ namespace AskTamMCP\Elementor\Data;
|
|||||||
defined('ABSPATH') || exit;
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data access layer for Elementor page data.
|
* Elementor data access layer.
|
||||||
*
|
*
|
||||||
* Read/write strategy:
|
* Methods ported from elementor-mcp v1.9.0 (class-elementor-data.php).
|
||||||
* CLI / DOING_CRON → raw $wpdb queries, bypassing WP object cache entirely.
|
* CLI/cron context uses raw $wpdb queries to bypass WP object cache —
|
||||||
* HTTP context → document->save() first; falls back to direct meta write
|
* the core fix for stale-read bugs in long-running build scripts.
|
||||||
* with wp_slash() + CSS cache invalidation on failure.
|
|
||||||
*
|
|
||||||
* Methods mirror the public API of Elementor_MCP_Data (v1.9.0) so both
|
|
||||||
* HTTP and CLI callers use identical call signatures.
|
|
||||||
*/
|
*/
|
||||||
class ElementorData {
|
class ElementorData {
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Document access
|
// get_document
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
public function get_document(int $post_id) {
|
public function get_document( int $post_id ) {
|
||||||
if (!class_exists('\Elementor\Plugin')) {
|
if ( ! class_exists( '\Elementor\Plugin' ) ) {
|
||||||
return new \WP_Error('elementor_missing', 'Elementor is not installed or active.');
|
return new \WP_Error( 'elementor_missing', 'Elementor is not installed or active.' );
|
||||||
}
|
}
|
||||||
$document = \Elementor\Plugin::$instance->documents->get($post_id);
|
$document = \Elementor\Plugin::$instance->documents->get( $post_id );
|
||||||
if (!$document) {
|
if ( ! $document ) {
|
||||||
return new \WP_Error('document_not_found', "Elementor document not found for post ID {$post_id}.");
|
return new \WP_Error(
|
||||||
|
'document_not_found',
|
||||||
|
sprintf( 'Elementor document not found for post ID %d.', $post_id )
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return $document;
|
return $document;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Read page data
|
// get_page_data
|
||||||
|
// Tries document API first, falls back to raw DB read for CLI/cron.
|
||||||
|
// Matches upstream v1.9.0 get_page_data() with our CLI-bypass layer.
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
public function get_page_data( int $post_id ) {
|
||||||
* Gets the element tree for an Elementor page.
|
// CLI/cron: bypass WP object cache entirely — prevents stale reads in
|
||||||
*
|
// long-running scripts where update_post_meta calls leave the cache
|
||||||
* CLI/cron: reads directly from $wpdb, bypassing WP object cache entirely.
|
// inconsistent (the root cause of elements overwriting each other).
|
||||||
* HTTP: tries document->get_elements_data(), falls back to get_post_meta(),
|
if ( PHP_SAPI === 'cli' || ( defined( 'DOING_CRON' ) && DOING_CRON ) ) {
|
||||||
* then raw $wpdb as last resort (handles all proxy/REST contexts).
|
|
||||||
*/
|
|
||||||
public function get_page_data(int $post_id) {
|
|
||||||
// CLI/cron: WP object cache stays stale after meta writes done outside
|
|
||||||
// document->save(). Always read raw from DB.
|
|
||||||
if (PHP_SAPI === 'cli' || (defined('DOING_CRON') && DOING_CRON)) {
|
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
$raw = $wpdb->get_var($wpdb->prepare(
|
$raw = $wpdb->get_var( $wpdb->prepare(
|
||||||
"SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id=%d AND meta_key='_elementor_data' LIMIT 1",
|
"SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id=%d AND meta_key='_elementor_data' LIMIT 1",
|
||||||
$post_id
|
$post_id
|
||||||
));
|
) );
|
||||||
if (!empty($raw) && is_string($raw)) {
|
if ( ! empty( $raw ) && is_string( $raw ) ) {
|
||||||
$decoded = json_decode($raw, true);
|
$decoded = json_decode( $raw, true );
|
||||||
if (is_array($decoded)) return $decoded;
|
if ( is_array( $decoded ) ) {
|
||||||
|
return $decoded;
|
||||||
}
|
}
|
||||||
return [];
|
}
|
||||||
|
return array();
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTP: try Elementor document API first (canonical path).
|
// Web context: try document API first (upstream v1.9.0 pattern).
|
||||||
$document = $this->get_document($post_id);
|
$document = $this->get_document( $post_id );
|
||||||
if (is_wp_error($document)) return $document;
|
if ( is_wp_error( $document ) ) {
|
||||||
|
return $document;
|
||||||
|
}
|
||||||
|
|
||||||
$data = $document->get_elements_data();
|
$data = $document->get_elements_data();
|
||||||
if (is_array($data) && !empty($data)) return $data;
|
if ( is_array( $data ) && ! empty( $data ) ) {
|
||||||
|
return $data;
|
||||||
// Fallback: raw get_post_meta (handles REST/proxy contexts).
|
|
||||||
$raw = get_post_meta($post_id, '_elementor_data', true);
|
|
||||||
if (!empty($raw) && is_string($raw)) {
|
|
||||||
$decoded = json_decode($raw, true);
|
|
||||||
if (is_array($decoded)) return $decoded;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Last resort: bypass object cache entirely.
|
// Fallback: read from raw post meta (handles proxy / REST contexts).
|
||||||
global $wpdb;
|
$raw = get_post_meta( $post_id, '_elementor_data', true );
|
||||||
$raw = $wpdb->get_var($wpdb->prepare(
|
if ( ! empty( $raw ) && is_string( $raw ) ) {
|
||||||
"SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id=%d AND meta_key='_elementor_data' LIMIT 1",
|
$decoded = json_decode( $raw, true );
|
||||||
$post_id
|
if ( is_array( $decoded ) ) {
|
||||||
));
|
return $decoded;
|
||||||
if (!empty($raw) && is_string($raw)) {
|
}
|
||||||
$decoded = json_decode($raw, true);
|
|
||||||
if (is_array($decoded)) return $decoded;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
return array();
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Save page data
|
// get_page_settings
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
public function get_page_settings( int $post_id ) {
|
||||||
* Saves element tree for an Elementor page.
|
$document = $this->get_document( $post_id );
|
||||||
*
|
if ( is_wp_error( $document ) ) {
|
||||||
* CLI/cron: raw $wpdb INSERT/UPDATE + object cache flush.
|
return $document;
|
||||||
* HTTP: document->save() first; on failure falls back to direct meta write
|
|
||||||
* with wp_slash() + CSS cache invalidation (mirrors v1.9.0 fallback).
|
|
||||||
*/
|
|
||||||
public function save_page_data(int $post_id, array $data) {
|
|
||||||
// CLI/cron: skip document->save() entirely.
|
|
||||||
// - avoids wp_update_post mail crash (no sendmail in container)
|
|
||||||
// - avoids WP object cache stale-write problem in long-running scripts
|
|
||||||
if (PHP_SAPI === 'cli' || (defined('DOING_CRON') && DOING_CRON)) {
|
|
||||||
global $wpdb;
|
|
||||||
$json = wp_json_encode($data);
|
|
||||||
$table = $wpdb->postmeta;
|
|
||||||
|
|
||||||
// _elementor_data
|
|
||||||
$exists = $wpdb->get_var($wpdb->prepare(
|
|
||||||
"SELECT meta_id FROM {$table} WHERE post_id=%d AND meta_key='_elementor_data' LIMIT 1",
|
|
||||||
$post_id
|
|
||||||
));
|
|
||||||
if ($exists) {
|
|
||||||
$wpdb->query($wpdb->prepare(
|
|
||||||
"UPDATE {$table} SET meta_value=%s WHERE meta_id=%d",
|
|
||||||
$json, (int)$exists
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
$wpdb->query($wpdb->prepare(
|
|
||||||
"INSERT INTO {$table} (post_id, meta_key, meta_value) VALUES (%d, '_elementor_data', %s)",
|
|
||||||
$post_id, $json
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// _elementor_edit_mode
|
|
||||||
$modeExists = $wpdb->get_var($wpdb->prepare(
|
|
||||||
"SELECT meta_id FROM {$table} WHERE post_id=%d AND meta_key='_elementor_edit_mode' LIMIT 1",
|
|
||||||
$post_id
|
|
||||||
));
|
|
||||||
if ($modeExists) {
|
|
||||||
$wpdb->query($wpdb->prepare(
|
|
||||||
"UPDATE {$table} SET meta_value='builder' WHERE meta_id=%d",
|
|
||||||
(int)$modeExists
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
$wpdb->query($wpdb->prepare(
|
|
||||||
"INSERT INTO {$table} (post_id, meta_key, meta_value) VALUES (%d, '_elementor_edit_mode', 'builder')",
|
|
||||||
$post_id
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// _elementor_version
|
|
||||||
if (defined('ELEMENTOR_VERSION')) {
|
|
||||||
$verExists = $wpdb->get_var($wpdb->prepare(
|
|
||||||
"SELECT meta_id FROM {$table} WHERE post_id=%d AND meta_key='_elementor_version' LIMIT 1",
|
|
||||||
$post_id
|
|
||||||
));
|
|
||||||
if ($verExists) {
|
|
||||||
$wpdb->query($wpdb->prepare(
|
|
||||||
"UPDATE {$table} SET meta_value=%s WHERE meta_id=%d",
|
|
||||||
ELEMENTOR_VERSION, (int)$verExists
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
$wpdb->query($wpdb->prepare(
|
|
||||||
"INSERT INTO {$table} (post_id, meta_key, meta_value) VALUES (%d, '_elementor_version', %s)",
|
|
||||||
$post_id, ELEMENTOR_VERSION
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invalidate _elementor_css meta so Elementor regenerates it.
|
|
||||||
$wpdb->query($wpdb->prepare(
|
|
||||||
"DELETE FROM {$table} WHERE post_id=%d AND meta_key='_elementor_css'",
|
|
||||||
$post_id
|
|
||||||
));
|
|
||||||
|
|
||||||
// Remove physical CSS file so front-end regenerates on next load.
|
|
||||||
$upload_dir = wp_get_upload_dir();
|
|
||||||
$css_path = $upload_dir['basedir'] . '/elementor/css/post-' . $post_id . '.css';
|
|
||||||
if (file_exists($css_path)) {
|
|
||||||
@unlink($css_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flush WP object cache for this post so next get_post_meta reads fresh.
|
|
||||||
wp_cache_delete($post_id, 'post_meta');
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTTP: attempt native Elementor save (handles CSS regen, cache busting).
|
|
||||||
$document = $this->get_document($post_id);
|
|
||||||
if (is_wp_error($document)) return $document;
|
|
||||||
|
|
||||||
$result = $document->save(['elements' => $data]);
|
|
||||||
|
|
||||||
if (false === $result) {
|
|
||||||
// Fallback: direct meta write for non-browser contexts (REST proxy etc.).
|
|
||||||
$json = wp_json_encode($data);
|
|
||||||
if (false === $json) {
|
|
||||||
return new \WP_Error('json_encode_failed', 'Failed to encode element data as JSON.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// wp_slash() is required — update_post_meta strips one level of slashes.
|
|
||||||
update_post_meta($post_id, '_elementor_data', wp_slash($json));
|
|
||||||
update_post_meta($post_id, '_elementor_edit_mode', 'builder');
|
|
||||||
|
|
||||||
if (defined('ELEMENTOR_VERSION')) {
|
|
||||||
update_post_meta($post_id, '_elementor_version', ELEMENTOR_VERSION);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invalidate Elementor CSS cache so it regenerates on next page view.
|
|
||||||
delete_post_meta($post_id, '_elementor_css');
|
|
||||||
|
|
||||||
$upload_dir = wp_get_upload_dir();
|
|
||||||
$css_path = $upload_dir['basedir'] . '/elementor/css/post-' . $post_id . '.css';
|
|
||||||
if (file_exists($css_path)) {
|
|
||||||
wp_delete_file($css_path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Page settings
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
public function get_page_settings(int $post_id) {
|
|
||||||
$document = $this->get_document($post_id);
|
|
||||||
if (is_wp_error($document)) return $document;
|
|
||||||
return $document->get_settings();
|
return $document->get_settings();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// -------------------------------------------------------------------------
|
||||||
* Saves page-level settings.
|
// save_page_data
|
||||||
*
|
// Upstream v1.9.0: try document->save() first; fall back to direct meta
|
||||||
* Tries native document->save() first; falls back to direct meta merge
|
// write with wp_slash() + CSS cache invalidation.
|
||||||
* for non-browser contexts (mirrors v1.9.0 save_page_settings fallback).
|
// CLI/cron uses raw $wpdb to avoid object-cache inconsistencies and the
|
||||||
*/
|
// wp_update_post email fatal (known issue #3 in 05-KNOWN-ISSUES.md).
|
||||||
public function save_page_settings(int $post_id, array $settings) {
|
// -------------------------------------------------------------------------
|
||||||
$document = $this->get_document($post_id);
|
|
||||||
if (is_wp_error($document)) return $document;
|
|
||||||
|
|
||||||
$result = $document->save(['settings' => $settings]);
|
public function save_page_data( int $post_id, array $data ) {
|
||||||
|
// CLI/cron fast path: raw DB write, bypasses object cache + document->save().
|
||||||
|
if ( PHP_SAPI === 'cli' || ( defined( 'DOING_CRON' ) && DOING_CRON ) ) {
|
||||||
|
global $wpdb;
|
||||||
|
$json = wp_json_encode( $data );
|
||||||
|
$table = $wpdb->postmeta;
|
||||||
|
|
||||||
if (false === $result) {
|
$exists = $wpdb->get_var( $wpdb->prepare(
|
||||||
$existing = get_post_meta($post_id, '_elementor_page_settings', true);
|
"SELECT meta_id FROM {$table} WHERE post_id=%d AND meta_key='_elementor_data' LIMIT 1",
|
||||||
if (!is_array($existing)) $existing = [];
|
$post_id
|
||||||
$merged = array_merge($existing, $settings);
|
) );
|
||||||
update_post_meta($post_id, '_elementor_page_settings', $merged);
|
if ( $exists ) {
|
||||||
delete_post_meta($post_id, '_elementor_css');
|
$wpdb->query( $wpdb->prepare(
|
||||||
|
"UPDATE {$table} SET meta_value=%s WHERE meta_id=%d",
|
||||||
|
$json, (int) $exists
|
||||||
|
) );
|
||||||
|
} else {
|
||||||
|
$wpdb->query( $wpdb->prepare(
|
||||||
|
"INSERT INTO {$table} (post_id, meta_key, meta_value) VALUES (%d, '_elementor_data', %s)",
|
||||||
|
$post_id, $json
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure Elementor meta flags are set.
|
||||||
|
$mode_exists = $wpdb->get_var( $wpdb->prepare(
|
||||||
|
"SELECT meta_id FROM {$table} WHERE post_id=%d AND meta_key='_elementor_edit_mode' LIMIT 1",
|
||||||
|
$post_id
|
||||||
|
) );
|
||||||
|
if ( $mode_exists ) {
|
||||||
|
$wpdb->query( $wpdb->prepare(
|
||||||
|
"UPDATE {$table} SET meta_value='builder' WHERE meta_id=%d",
|
||||||
|
(int) $mode_exists
|
||||||
|
) );
|
||||||
|
} else {
|
||||||
|
$wpdb->query( $wpdb->prepare(
|
||||||
|
"INSERT INTO {$table} (post_id, meta_key, meta_value) VALUES (%d, '_elementor_edit_mode', 'builder')",
|
||||||
|
$post_id
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( defined( 'ELEMENTOR_VERSION' ) ) {
|
||||||
|
$ver_exists = $wpdb->get_var( $wpdb->prepare(
|
||||||
|
"SELECT meta_id FROM {$table} WHERE post_id=%d AND meta_key='_elementor_version' LIMIT 1",
|
||||||
|
$post_id
|
||||||
|
) );
|
||||||
|
if ( $ver_exists ) {
|
||||||
|
$wpdb->query( $wpdb->prepare(
|
||||||
|
"UPDATE {$table} SET meta_value=%s WHERE meta_id=%d",
|
||||||
|
ELEMENTOR_VERSION, (int) $ver_exists
|
||||||
|
) );
|
||||||
|
} else {
|
||||||
|
$wpdb->query( $wpdb->prepare(
|
||||||
|
"INSERT INTO {$table} (post_id, meta_key, meta_value) VALUES (%d, '_elementor_version', %s)",
|
||||||
|
$post_id, ELEMENTOR_VERSION
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalidate Elementor CSS cache so it regenerates on next page view.
|
||||||
|
$wpdb->query( $wpdb->prepare(
|
||||||
|
"DELETE FROM {$table} WHERE post_id=%d AND meta_key='_elementor_css'",
|
||||||
|
$post_id
|
||||||
|
) );
|
||||||
|
|
||||||
|
$upload_dir = wp_get_upload_dir();
|
||||||
|
$css_path = $upload_dir['basedir'] . '/elementor/css/post-' . $post_id . '.css';
|
||||||
|
if ( file_exists( $css_path ) ) {
|
||||||
|
wp_delete_file( $css_path );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalidate WP object cache so next get_post_meta in same process reads fresh.
|
||||||
|
wp_cache_delete( $post_id, 'post_meta' );
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Web context: try Elementor native save (handles CSS regen, cache busting).
|
||||||
|
$document = $this->get_document( $post_id );
|
||||||
|
if ( is_wp_error( $document ) ) {
|
||||||
|
return $document;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $document->save( array( 'elements' => $data ) );
|
||||||
|
|
||||||
|
if ( false === $result ) {
|
||||||
|
// Fallback: direct meta write for non-browser contexts (REST proxy).
|
||||||
|
// wp_slash() is required — WordPress double-unslashes on save.
|
||||||
|
$json = wp_json_encode( $data );
|
||||||
|
if ( false === $json ) {
|
||||||
|
return new \WP_Error( 'json_encode_failed', 'Failed to encode element data as JSON.' );
|
||||||
|
}
|
||||||
|
|
||||||
|
update_post_meta( $post_id, '_elementor_data', wp_slash( $json ) );
|
||||||
|
update_post_meta( $post_id, '_elementor_edit_mode', 'builder' );
|
||||||
|
|
||||||
|
if ( defined( 'ELEMENTOR_VERSION' ) ) {
|
||||||
|
update_post_meta( $post_id, '_elementor_version', ELEMENTOR_VERSION );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalidate CSS cache.
|
||||||
|
delete_post_meta( $post_id, '_elementor_css' );
|
||||||
|
|
||||||
|
$upload_dir = wp_get_upload_dir();
|
||||||
|
$css_path = $upload_dir['basedir'] . '/elementor/css/post-' . $post_id . '.css';
|
||||||
|
if ( file_exists( $css_path ) ) {
|
||||||
|
wp_delete_file( $css_path );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Widget registry
|
// save_page_settings
|
||||||
|
// Upstream v1.9.0: try document->save(); fall back to direct meta merge.
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
public function get_registered_widgets(): array {
|
public function save_page_settings( int $post_id, array $settings ) {
|
||||||
if (!class_exists('\Elementor\Plugin')) return [];
|
$document = $this->get_document( $post_id );
|
||||||
$widgets = \Elementor\Plugin::$instance->widgets_manager->get_widget_types();
|
if ( is_wp_error( $document ) ) {
|
||||||
$result = [];
|
return $document;
|
||||||
foreach ($widgets as $widget) {
|
|
||||||
$result[] = ['name' => $widget->get_name(), 'title' => $widget->get_title()];
|
|
||||||
}
|
}
|
||||||
return $result;
|
|
||||||
|
$result = $document->save( array( 'settings' => $settings ) );
|
||||||
|
|
||||||
|
if ( false === $result ) {
|
||||||
|
$existing = get_post_meta( $post_id, '_elementor_page_settings', true );
|
||||||
|
if ( ! is_array( $existing ) ) {
|
||||||
|
$existing = array();
|
||||||
|
}
|
||||||
|
update_post_meta( $post_id, '_elementor_page_settings', array_merge( $existing, $settings ) );
|
||||||
|
delete_post_meta( $post_id, '_elementor_css' );
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// ID generation
|
// find_element (upstream: find_element_by_id)
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
public function find_element( array $elements, string $element_id ): ?array {
|
||||||
* Generates a 7-character random hex string matching Elementor's format.
|
foreach ( $elements as $element ) {
|
||||||
* Uses random_bytes() for proper entropy (mirrors Elementor_MCP_Id_Generator).
|
if ( ( $element['id'] ?? '' ) === $element_id ) {
|
||||||
*/
|
return $element;
|
||||||
public function generate_id(): string {
|
}
|
||||||
return substr(bin2hex(random_bytes(4)), 0, 7);
|
if ( ! empty( $element['elements'] ) && is_array( $element['elements'] ) ) {
|
||||||
|
$found = $this->find_element( $element['elements'], $element_id );
|
||||||
|
if ( null !== $found ) {
|
||||||
|
return $found;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Element tree helpers
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively finds an element by ID within a tree.
|
|
||||||
* Mirrors find_element_by_id() from Elementor_MCP_Data v1.9.0.
|
|
||||||
*/
|
|
||||||
public function find_element(array $elements, string $element_id): ?array {
|
|
||||||
foreach ($elements as $element) {
|
|
||||||
if (($element['id'] ?? '') === $element_id) return $element;
|
|
||||||
if (!empty($element['elements'])) {
|
|
||||||
$found = $this->find_element($element['elements'], $element_id);
|
|
||||||
if ($found !== null) return $found;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// -------------------------------------------------------------------------
|
||||||
* Inserts an element into the tree under parent_id at position.
|
// insert_element (upstream: insert_element — by reference)
|
||||||
* Empty parent_id = top-level insertion.
|
// Supports top-level insertion (empty parent_id) and nested.
|
||||||
* Mirrors insert_element() from Elementor_MCP_Data v1.9.0 (by-reference).
|
// -------------------------------------------------------------------------
|
||||||
*/
|
|
||||||
public function insert_element(array &$data, string $parent_id, array $element, int $position = -1): bool {
|
public function insert_element( array &$data, string $parent_id, array $element, int $position = -1 ): bool {
|
||||||
if (empty($parent_id)) {
|
// Top-level insertion.
|
||||||
if ($position < 0 || $position >= count($data)) {
|
if ( empty( $parent_id ) ) {
|
||||||
|
if ( $position < 0 || $position >= count( $data ) ) {
|
||||||
$data[] = $element;
|
$data[] = $element;
|
||||||
} else {
|
} else {
|
||||||
array_splice($data, $position, 0, [$element]);
|
array_splice( $data, $position, 0, array( $element ) );
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($data as &$item) {
|
// Find parent and insert.
|
||||||
if (isset($item['id']) && $item['id'] === $parent_id) {
|
foreach ( $data as &$item ) {
|
||||||
if (!isset($item['elements'])) $item['elements'] = [];
|
if ( isset( $item['id'] ) && $item['id'] === $parent_id ) {
|
||||||
if ($position < 0 || $position >= count($item['elements'])) {
|
if ( ! isset( $item['elements'] ) ) {
|
||||||
|
$item['elements'] = array();
|
||||||
|
}
|
||||||
|
if ( $position < 0 || $position >= count( $item['elements'] ) ) {
|
||||||
$item['elements'][] = $element;
|
$item['elements'][] = $element;
|
||||||
} else {
|
} else {
|
||||||
array_splice($item['elements'], $position, 0, [$element]);
|
array_splice( $item['elements'], $position, 0, array( $element ) );
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (!empty($item['elements']) && is_array($item['elements'])) {
|
if ( ! empty( $item['elements'] ) && is_array( $item['elements'] ) ) {
|
||||||
if ($this->insert_element($item['elements'], $parent_id, $element, $position)) {
|
if ( $this->insert_element( $item['elements'], $parent_id, $element, $position ) ) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -324,18 +292,18 @@ class ElementorData {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// -------------------------------------------------------------------------
|
||||||
* Removes an element from the tree by ID.
|
// remove_element (upstream: remove_element — by reference)
|
||||||
* Mirrors remove_element() from Elementor_MCP_Data v1.9.0 (by-reference).
|
// -------------------------------------------------------------------------
|
||||||
*/
|
|
||||||
public function remove_element(array &$data, string $element_id): bool {
|
public function remove_element( array &$data, string $element_id ): bool {
|
||||||
foreach ($data as $index => &$item) {
|
foreach ( $data as $index => &$item ) {
|
||||||
if (isset($item['id']) && $item['id'] === $element_id) {
|
if ( isset( $item['id'] ) && $item['id'] === $element_id ) {
|
||||||
array_splice($data, $index, 1);
|
array_splice( $data, $index, 1 );
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (!empty($item['elements']) && is_array($item['elements'])) {
|
if ( ! empty( $item['elements'] ) && is_array( $item['elements'] ) ) {
|
||||||
if ($this->remove_element($item['elements'], $element_id)) {
|
if ( $this->remove_element( $item['elements'], $element_id ) ) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -343,26 +311,32 @@ class ElementorData {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// -------------------------------------------------------------------------
|
||||||
* Updates settings for a specific element in the tree (by-reference).
|
// update_element_settings (upstream: update_element_settings — by reference)
|
||||||
*
|
// Normalises container flex-key aliases before merging (issue #32 fix from
|
||||||
* Applies container key alias normalisation (justify_content → flex_justify_content
|
// upstream v1.5.1): justify_content→flex_justify_content, etc.
|
||||||
* etc.) before merging — mirrors update_element_settings() v1.9.0 which calls
|
// -------------------------------------------------------------------------
|
||||||
* normalize_container_settings() when elType=container.
|
|
||||||
*/
|
public function update_element_settings( array &$data, string $element_id, array $settings, bool $merge = true ): bool {
|
||||||
public function update_element_settings(array &$data, string $element_id, array $settings, bool $merge = true): bool {
|
foreach ( $data as &$item ) {
|
||||||
foreach ($data as &$item) {
|
if ( isset( $item['id'] ) && $item['id'] === $element_id ) {
|
||||||
if (($item['id'] ?? '') === $element_id) {
|
if ( ! isset( $item['settings'] ) ) {
|
||||||
// Normalise flex shorthand keys for containers.
|
$item['settings'] = array();
|
||||||
if (($item['elType'] ?? '') === 'container') {
|
|
||||||
$settings = self::normalize_container_settings($settings);
|
|
||||||
}
|
}
|
||||||
$current = $item['settings'] ?? [];
|
|
||||||
$item['settings'] = $merge ? array_merge($current, $settings) : $settings;
|
// Normalise container flex shorthand keys (upstream v1.5.1 fix).
|
||||||
|
if ( 'container' === ( $item['elType'] ?? '' ) ) {
|
||||||
|
$settings = $this->normalize_container_settings( $settings );
|
||||||
|
}
|
||||||
|
|
||||||
|
$item['settings'] = $merge
|
||||||
|
? array_merge( $item['settings'], $settings )
|
||||||
|
: $settings;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (!empty($item['elements']) && is_array($item['elements'])) {
|
if ( ! empty( $item['elements'] ) && is_array( $item['elements'] ) ) {
|
||||||
if ($this->update_element_settings($item['elements'], $element_id, $settings, $merge)) {
|
if ( $this->update_element_settings( $item['elements'], $element_id, $settings, $merge ) ) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -370,74 +344,84 @@ class ElementorData {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// -------------------------------------------------------------------------
|
||||||
* Recursively reassigns fresh IDs to all elements in a tree.
|
// normalize_container_settings
|
||||||
* Mirrors reassign_ids() from Elementor_MCP_Data v1.9.0.
|
// Upstream v1.9.0 Element_Factory::normalize_container_settings().
|
||||||
*/
|
// Rewrites unprefixed flex shorthand keys to the keys Elementor CSS
|
||||||
public function reassign_ids(array $elements): array {
|
// generator actually reads (issue #32).
|
||||||
foreach ($elements as &$element) {
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
public function normalize_container_settings( array $settings ): array {
|
||||||
|
$aliases = array(
|
||||||
|
'justify_content' => 'flex_justify_content',
|
||||||
|
'align_items' => 'flex_align_items',
|
||||||
|
'align_content' => 'flex_align_content',
|
||||||
|
);
|
||||||
|
foreach ( $aliases as $shorthand => $flex_key ) {
|
||||||
|
if ( ! array_key_exists( $shorthand, $settings ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ( ! array_key_exists( $flex_key, $settings ) ) {
|
||||||
|
$settings[ $flex_key ] = $settings[ $shorthand ];
|
||||||
|
}
|
||||||
|
unset( $settings[ $shorthand ] );
|
||||||
|
}
|
||||||
|
return $settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// reassign_ids (upstream: reassign_ids + reassign_element_ids)
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
public function reassign_ids( array $elements ): array {
|
||||||
|
foreach ( $elements as &$element ) {
|
||||||
$element['id'] = $this->generate_id();
|
$element['id'] = $this->generate_id();
|
||||||
if (!empty($element['elements']) && is_array($element['elements'])) {
|
if ( ! empty( $element['elements'] ) && is_array( $element['elements'] ) ) {
|
||||||
$element['elements'] = $this->reassign_ids($element['elements']);
|
$element['elements'] = $this->reassign_ids( $element['elements'] );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $elements;
|
return $elements;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// -------------------------------------------------------------------------
|
||||||
* Reassigns a fresh ID to a single element and all its children.
|
// count_elements (upstream: count_elements)
|
||||||
* Mirrors reassign_element_ids() from Elementor_MCP_Data v1.9.0.
|
// -------------------------------------------------------------------------
|
||||||
*/
|
|
||||||
public function reassign_element_ids(array $element): array {
|
|
||||||
$element['id'] = $this->generate_id();
|
|
||||||
if (!empty($element['elements']) && is_array($element['elements'])) {
|
|
||||||
$element['elements'] = $this->reassign_ids($element['elements']);
|
|
||||||
}
|
|
||||||
return $element;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
public function count_elements( array $elements ): int {
|
||||||
* Recursively counts all elements in a tree.
|
$count = count( $elements );
|
||||||
*/
|
foreach ( $elements as $element ) {
|
||||||
public function count_elements(array $elements): int {
|
if ( ! empty( $element['elements'] ) && is_array( $element['elements'] ) ) {
|
||||||
$count = count($elements);
|
$count += $this->count_elements( $element['elements'] );
|
||||||
foreach ($elements as $element) {
|
|
||||||
if (!empty($element['elements'])) {
|
|
||||||
$count += $this->count_elements($element['elements']);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $count;
|
return $count;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Container key normalisation
|
// get_registered_widgets
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
public function get_registered_widgets(): array {
|
||||||
* Rewrites unprefixed flex shorthand keys to the prefixed keys that
|
if ( ! class_exists( '\Elementor\Plugin' ) ) {
|
||||||
* Elementor's container CSS generator actually reads.
|
return array();
|
||||||
*
|
}
|
||||||
* Without this, justify_content / align_items / align_content are persisted
|
$widgets = \Elementor\Plugin::$instance->widgets_manager->get_widget_types();
|
||||||
* under names Elementor ignores and the container renders with default
|
$result = array();
|
||||||
* alignment on the front-end.
|
foreach ( $widgets as $widget ) {
|
||||||
*
|
$result[] = array(
|
||||||
* Mirrors CONTAINER_KEY_ALIASES + normalize_container_settings() from
|
'name' => $widget->get_name(),
|
||||||
* Elementor_MCP_Element_Factory v1.9.0.
|
'title' => $widget->get_title(),
|
||||||
*/
|
);
|
||||||
private const CONTAINER_KEY_ALIASES = [
|
}
|
||||||
'justify_content' => 'flex_justify_content',
|
return $result;
|
||||||
'align_items' => 'flex_align_items',
|
}
|
||||||
'align_content' => 'flex_align_content',
|
|
||||||
];
|
|
||||||
|
|
||||||
public static function normalize_container_settings(array $settings): array {
|
// -------------------------------------------------------------------------
|
||||||
foreach (self::CONTAINER_KEY_ALIASES as $shorthand => $flex_key) {
|
// generate_id (upstream: Elementor_MCP_Id_Generator::generate)
|
||||||
if (!array_key_exists($shorthand, $settings)) continue;
|
// 7-char hex matching Elementor's internal format.
|
||||||
if (!array_key_exists($flex_key, $settings)) {
|
// -------------------------------------------------------------------------
|
||||||
$settings[$flex_key] = $settings[$shorthand];
|
|
||||||
}
|
public function generate_id(): string {
|
||||||
unset($settings[$shorthand]);
|
return substr( bin2hex( random_bytes( 4 ) ), 0, 7 );
|
||||||
}
|
|
||||||
return $settings;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user