Created
August 17, 2025 13:08
-
-
Save mlbd/bf2572ffce9b2e5c8f52a029eab71d37 to your computer and use it in GitHub Desktop.
Fix Missing Elementor Repeater Images After OCDI Import (Multisite-Safe Reconciler)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Run the Elementor media reconciliation AFTER One Click Demo Import finishes. | |
* | |
* Why priority 30? | |
* - Ensures your own after-import tasks (menus, front/blog pages, Replace URL, etc.) | |
* have already completed, so media IDs/URLs are normalized last. | |
* | |
* Hook names: | |
* - 'ocdi/after_import' → newer hook name used by OCDI. | |
* - 'pt-ocdi/after_import' → legacy hook name (older plugin versions). | |
* | |
* Note: | |
* - We accept 1 argument ($selected_import) for compatibility with OCDI, | |
* even though we don't use it here. | |
* - If you already call uxora_reconcile_all_elementor_media() inside | |
* uxora_after_import_setup(), remove one call to avoid double work. | |
*/ | |
// Newer hook name. | |
add_action( 'ocdi/after_import', function( $selected_import ) { | |
uxora_reconcile_all_elementor_media(); | |
}, 30, 1 ); | |
// Legacy hook name (kept for backward compatibility). | |
add_action( 'pt-ocdi/after_import', function( $selected_import ) { | |
uxora_reconcile_all_elementor_media(); | |
}, 30, 1 ); | |
/** | |
* Elementor media reconciliation helpers. | |
* | |
* Goal: | |
* - Make every Elementor media control's ['id','url'] pair consistent so Elementor | |
* renders the intended image. Elementor prefers the attachment ID; if an 'id' | |
* points to a valid local attachment, it will regenerate the URL from that ID. | |
* - After migrations/imports, IDs and URLs can fall out of sync (especially inside | |
* nested repeater controls). This routine normalizes them. | |
* | |
* Works with: | |
* - Single-site: URLs like /wp-content/uploads/... | |
* - Multisite: URLs like /wp-content/uploads/sites/{blog_id}/... (expected) | |
* | |
* Usage: | |
* - Run once after import/migration (e.g., temporarily from admin_init). | |
* - Then remove the one-time trigger. The functions themselves are safe to keep. | |
*/ | |
/** | |
* Reconcile Elementor media across all Elementor-bearing posts. | |
* | |
* @return void | |
*/ | |
function uxora_reconcile_all_elementor_media() : void { | |
// Query all common post types that may carry Elementor data. | |
$q = new WP_Query( [ | |
'post_type' => [ 'page', 'post', 'elementor_library' ], | |
'posts_per_page' => -1, | |
'post_status' => 'any', | |
'fields' => 'ids', | |
] ); | |
foreach ( $q->posts as $post_id ) { | |
// error_log( "[UXORA] reconcile start post {$post_id}" ); | |
uxora_reconcile_elementor_media_for_post( (int) $post_id ); | |
} | |
wp_reset_postdata(); | |
// Clear Elementor's CSS/assets cache so changes are reflected immediately. | |
if ( class_exists( '\Elementor\Plugin' ) ) { | |
\Elementor\Plugin::$instance->files_manager->clear_cache(); | |
} | |
} | |
/** | |
* Reconcile Elementor media for a single post. | |
* | |
* What it does per post meta key: | |
* 1) Load Elementor meta (JSON string or array). | |
* 2) Walk the entire structure (widgets, controls, repeaters). | |
* 3) For each media-like array: | |
* - If it has a valid 'id', normalize 'url' to wp_get_attachment_url( id ). | |
* - Else if it only has 'url' that maps to a local attachment, set the 'id' and normalize 'url'. | |
* - Else leave external/unattached URLs as-is. | |
* 4) Save back using the original storage format (JSON for _elementor_data; array or JSON for settings). | |
* | |
* @param int $post_id The post to process. | |
* @return void | |
*/ | |
function uxora_reconcile_elementor_media_for_post( int $post_id ) : void { | |
// Elementor stores content under _elementor_data (JSON) and | |
// per-page settings under _elementor_page_settings (array or JSON). | |
$meta_keys = [ '_elementor_data', '_elementor_page_settings' ]; | |
foreach ( $meta_keys as $meta_key ) { | |
$raw = get_post_meta( $post_id, $meta_key, true ); | |
if ( ! $raw ) { | |
continue; // Nothing to process. | |
} | |
// Track whether the original value was a JSON string so we can save in the same format. | |
$raw_was_json = is_string( $raw ); | |
$data = $raw_was_json ? json_decode( $raw, true ) : $raw; | |
if ( ! is_array( $data ) ) { | |
continue; // Malformed or non-array data; skip safely. | |
} | |
$changed = false; | |
/** | |
* Recursive walker that traverses the Elementor structure and reconciles media controls. | |
* | |
* @param mixed $node Current subtree. | |
* @return mixed Reconciled subtree. | |
*/ | |
$walk = function ( $node ) use ( &$walk, &$changed, $post_id ) { | |
if ( is_array( $node ) ) { | |
// Typical Elementor media control: ['id' => 123, 'url' => 'https://...'] | |
$has_url = isset( $node['url'] ) && is_string( $node['url'] ) && $node['url'] !== ''; | |
$has_id = isset( $node['id'] ) && (int) $node['id'] > 0; | |
if ( $has_id || $has_url ) { | |
$id = $has_id ? (int) $node['id'] : 0; | |
$url = $has_url ? (string) $node['url'] : ''; | |
// Normalize either side using WordPress core resolvers. | |
$url_from_id = $id ? wp_get_attachment_url( $id ) : ''; | |
$id_from_url = $url ? attachment_url_to_postid( $url ) : 0; | |
if ( $id && $url_from_id ) { | |
// We trust the ID and make the stored 'url' match WordPress. | |
if ( ! $has_url || $url !== $url_from_id ) { | |
$node['url'] = $url_from_id; | |
$changed = true; | |
} | |
} elseif ( ! $id && $id_from_url ) { | |
// No ID stored, but URL points to a local attachment: fix both. | |
$node['id'] = $id_from_url; | |
$node['url'] = wp_get_attachment_url( $id_from_url ) ?: $url; | |
$changed = true; | |
} else { | |
// External URL or unattached file: leave as-is (Elementor will use the URL). | |
// No change needed. | |
} | |
} | |
// Recurse into all children. | |
foreach ( $node as $k => $v ) { | |
$node[ $k ] = $walk( $v ); | |
} | |
} | |
return $node; | |
}; | |
$new = $walk( $data ); | |
// Save only if something changed. | |
if ( $changed ) { | |
if ( '_elementor_data' === $meta_key ) { | |
// _elementor_data must be JSON-encoded. | |
update_post_meta( $post_id, $meta_key, wp_slash( wp_json_encode( $new ) ) ); | |
} else { | |
// For settings, preserve the original storage format. | |
if ( $raw_was_json ) { | |
update_post_meta( $post_id, $meta_key, wp_slash( wp_json_encode( $new ) ) ); | |
} else { | |
update_post_meta( $post_id, $meta_key, $new ); | |
} | |
} | |
// error_log( "[UXORA] reconciled {$meta_key} for post {$post_id}" ); | |
} else { | |
// error_log( "[UXORA] no changes in {$meta_key} for post {$post_id}" ); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment