Skip to content

fix(duplication): preserve Elementor Kit postmeta across all URL replacement passes#803

Merged
superdav42 merged 2 commits intomainfrom
bugfix/elementor-kit-duplication
Apr 12, 2026
Merged

fix(duplication): preserve Elementor Kit postmeta across all URL replacement passes#803
superdav42 merged 2 commits intomainfrom
bugfix/elementor-kit-duplication

Conversation

@superdav42
Copy link
Copy Markdown
Collaborator

@superdav42 superdav42 commented Apr 12, 2026

Problem

When cloning a site that uses Elementor, the _elementor_page_settings postmeta for post_id=3 (the Elementor Global Kit) loses ~748 bytes. Origin template has 6,387 bytes; clone receives 5,639. Cloned sites render with Elementor's built-in default styles instead of the template's brand colours and typography.

Reproduce:

SELECT 'origin', LENGTH(meta_value) FROM wp_ORIGIN_postmeta
  WHERE post_id=3 AND meta_key='_elementor_page_settings'
UNION ALL
SELECT 'clone', LENGTH(meta_value) FROM wp_CLONE_postmeta
  WHERE post_id=3 AND meta_key='_elementor_page_settings';

Root causes

1. Stale WHERE clause in MUCD_Data::update()

The method built UPDATE table SET field = %s WHERE field = %s using the full serialized meta_value in the WHERE. With 7+ replacement passes (upload URL, blog URL, DB prefix, plus JSON-escaped variants for each), pass 1 rewrites the row. Pass 2 then searches for the original value — which no longer exists — and silently skips. The row is left with only the first pass's changes. On large TEXT columns, full-value equality comparison can also trigger engine-level truncation.

2. Silent data destruction on @unserialize() failure

try_replace() called @unserialize() and never checked for false. If unserialize failed (malformed or partially-corrupted input), the code continued with $row[$field] = false, then serialize(false)'b:0;' — destroying the entire value.

Fix

inc/duplication/data.php

  • MUCD_Data::get_primary_key($table): new static method with per-table static cache. Queries SHOW KEYS ... WHERE Key_name = 'PRIMARY' to get the PK column (e.g. meta_id for postmeta, ID for posts, option_id for options).
  • MUCD_Data::update(): uses WHERE pk = %s when a primary key is detectable; falls back to full-value WHERE for tables without a detectable PK. Also skips the UPDATE call entirely when try_replace() produces no change.
  • MUCD_Data::try_replace(): checks @unserialize() return for false (with special handling for the legitimate b:0; case) and returns the original value unchanged rather than re-serialising false.

inc/compat/class-elementor-compat.php

regenerate_css() gains a DB-level fallback. Duplication runs in the network admin context where \Elementor\Plugin is not loaded. Previously regenerate_css() returned early with no effect. The fallback deletes _elementor_css postmeta and the _elementor_global_css option so Elementor regenerates its compiled CSS on the first front-end visit.

Tests

22 tests / 85 assertions, all passing.

New: test_update_preserves_elementor_kit_byte_count — the regression test for this exact report. Inserts a realistic Kit payload (~6,387 bytes) into wp_postmeta, runs all 7 URL replacement passes through MUCD_Data::update(), then asserts:

  • All 4 system colours (#6EC1E4, #54595F, #7A7A7A, #61CE70) survive intact
  • All 4 typography entries (Roboto, Roboto Slab, Open Sans, Montserrat) survive intact
  • Logo and favicon URLs are rewritten to the clone
  • custom_css brand colours survive; hero background URL is rewritten
  • Byte count changes only by the predictable URL-length delta, not by a large unexpected amount

Additional unit tests: test_get_primary_key_returns_correct_column, test_get_primary_key_is_cached, test_try_replace_elementor_kit_page_settings, test_try_replace_multiple_passes_preserve_data, test_try_replace_returns_original_on_unserialize_failure, test_try_replace_serialized_false_not_treated_as_error.

Files

  • EDIT: inc/duplication/data.php — primary key lookup + update fix + unserialize guard
  • EDIT: inc/compat/class-elementor-compat.php — DB fallback for CSS cache clear
  • EDIT: tests/WP_Ultimo/Duplication/MUCD_Data_Test.php — 8 new tests

Verification

vendor/bin/phpunit --no-coverage --filter MUCD_Data_Test
# Expected: OK (22 tests, 85 assertions)

Summary by CodeRabbit

  • Bug Fixes

    • Enhanced Elementor cache invalidation during site duplication with improved fallback handling.
    • Improved serialized data handling to preserve data integrity during duplication operations.
  • Improvements

    • Optimized database operations during site duplication with automatic primary key detection.
    • Added robust handling for complex nested data structures during duplication.
  • Tests

    • Expanded test coverage for data duplication scenarios, including Elementor-specific edge cases.

…acement passes

MUCD_Data::update() previously used `WHERE meta_value = %s` with the
full serialized field value to identify rows for UPDATE. With 7+
replacement passes (upload URL, blog URL, prefix, JSON-escaped variants),
the second pass would search for the original value that had already been
rewritten by pass 1 — finding nothing and silently skipping. On large
TEXT columns this also risks engine-level comparison truncation.

For the Elementor Kit post (post_id=3, meta_key=_elementor_page_settings),
this caused 748 bytes of global settings — brand colors, typography,
custom CSS — to be lost on clone. Cloned sites fell back to Elementor's
built-in defaults instead of the template's styles.

Changes:
- MUCD_Data::get_primary_key(): new static method with per-table cache
  that queries SHOW KEYS to get the PRIMARY column name.
- MUCD_Data::update(): use `WHERE pk = %s` when a primary key is
  available (postmeta → meta_id, options → option_id, posts → ID).
  Falls back to full-value WHERE for tables without a detectable PK.
  Skips UPDATE entirely when try_replace() produces no change (avoids
  unnecessary writes).
- MUCD_Data::try_replace(): guard against @unserialize() returning false
  on malformed data — return the original value instead of re-serializing
  false (which would produce 'b:0;' and destroy the row).
- Elementor_Compat::regenerate_css(): add DB-level fallback for clearing
  Elementor CSS cache when \Elementor\Plugin is not loaded (the common
  case in network admin where duplication runs).

Tests (22 tests, 85 assertions):
- test_update_preserves_elementor_kit_byte_count: integration test that
  inserts a realistic ~6387-byte Kit payload into wp_postmeta, runs all
  7 URL replacement passes via MUCD_Data::update(), then asserts colors
  (#6EC1E4, #54595F, #7A7A7A, #61CE70), typography (Roboto, Open Sans,
  Montserrat), and URL rewrites are all intact and byte count changes
  only by the predictable URL-length delta.
- test_get_primary_key_returns_correct_column / _is_cached
- test_try_replace_elementor_kit_page_settings
- test_try_replace_multiple_passes_preserve_data
- test_try_replace_returns_original_on_unserialize_failure
- test_try_replace_serialized_false_not_treated_as_error
@superdav42 superdav42 added the origin:interactive Created by interactive user session label Apr 12, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 12, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 523252ce-c85d-413d-85f6-55fdce23dd15

📥 Commits

Reviewing files that changed from the base of the PR and between 78e8f9c and 639c7a9.

📒 Files selected for processing (3)
  • inc/compat/class-elementor-compat.php
  • inc/duplication/data.php
  • tests/WP_Ultimo/Duplication/MUCD_Data_Test.php

📝 Walkthrough

Walkthrough

The pull request enhances cache invalidation and URL replacement logic across site duplication. The Elementor compatibility layer now implements a DB-based fallback for CSS cache clearing when the Elementor API is unavailable. Data duplication improvements add primary-key detection and refine serialized data handling to preserve data integrity across nested structures.

Changes

Cohort / File(s) Summary
Elementor Cache Invalidation
inc/compat/class-elementor-compat.php
Modified regenerate_css() to always require site_id, switch to duplicated site context, and attempt cache invalidation via Elementor's files_manager. Added DB-based fallback that clears _elementor_css postmeta and related options when the API path is unavailable.
Data Duplication & URL Replacement
inc/duplication/data.php
Added get_primary_key() method to detect and cache table primary keys. Enhanced update() to use primary-key-based WHERE clauses instead of field-value matching. Improved try_replace() serialized-value handling to distinguish between unserialize failures and legitimate serialized false values, with nested deserialization support.
Test Coverage for Duplication Logic
tests/WP_Ultimo/Duplication/MUCD_Data_Test.php
Added six comprehensive test cases covering serialized-value edge cases (unserialize failures, nested serialization, multiple passes), Elementor Kit page settings validation, primary-key detection and caching, and a DB-level integration test verifying data integrity across replacement passes.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 Hoppy hops through data streams so deep,
Primary keys and serialized sleep,
Cache clears swift with fallback grace,
URLs dance through every place!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main fix: preserving Elementor Kit postmeta during URL replacement passes in the duplication process.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bugfix/elementor-kit-duplication

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

🔨 Build Complete - Ready for Testing!

📦 Download Build Artifact (Recommended)

Download the zip build, upload to WordPress and test:

🌐 Test in WordPress Playground (Very Experimental)

Click the link below to instantly test this PR in your browser - no installation needed!
Playground support for multisite is very limitied, hopefully it will get better in the future.

🚀 Launch in Playground

Login credentials: admin / password

@superdav42 superdav42 merged commit 5e73fc1 into main Apr 12, 2026
11 checks passed
@superdav42
Copy link
Copy Markdown
Collaborator Author

Problem

When cloning a site that uses Elementor, the _elementor_page_settings postmeta for post_id=3 (the Elementor Global Kit) loses ~748 bytes. Origin template has 6,387 bytes; clone receives 5,639. Cloned sites render with Elementor's built-in default styles instead of the template's brand colours and typography.
Reproduce:

SELECT 'origin', LENGTH(meta_value) FROM wp_ORIGIN_postmeta
  WHERE post_id=3 AND meta_key='_elementor_page_settings'
UNION ALL
SELECT 'clone', LENGTH(meta_value) FROM wp_CLONE_postmeta
  WHERE post_id=3 AND meta_key='_elementor_page_settings';

Root causes

1. Stale WHERE clause in MUCD_Data::update()
The method built UPDATE table SET field = %s WHERE field = %s using the full serialized meta_value in the WHERE. With 7+ replacement passes (upload URL, blog URL, DB prefix, plus JSON-escaped variants for each), pass 1 rewrites the row. Pass 2 then searches for the original value — which no longer exists — and silently skips. The row is left with only the first pass's changes. On large TEXT columns, full-value equality comparison can also trigger engine-level truncation.
2. Silent data destruction on @unserialize() failure
try_replace() called @unserialize() and never checked for false. If unserialize failed (malformed or partially-corrupted input), the code continued with $row[$field] = false, then serialize(false)'b:0;' — destroying the entire value.

Fix

inc/duplication/data.php

  • MUCD_Data::get_primary_key($table): new static method with per-table static cache. Queries SHOW KEYS ... WHERE Key_name = 'PRIMARY' to get the PK column (e.g. meta_id for postmeta, ID for posts, option_id for options).
  • MUCD_Data::update(): uses WHERE pk = %s when a primary key is detectable; falls back to full-value WHERE for tables without a detectable PK. Also skips the UPDATE call entirely when try_replace() produces no change.
  • MUCD_Data::try_replace(): checks @unserialize() return for false (with special handling for the legitimate b:0; case) and returns the original value unchanged rather than re-serialising false.
    inc/compat/class-elementor-compat.php
    regenerate_css() gains a DB-level fallback. Duplication runs in the network admin context where \Elementor\Plugin is not loaded. Previously regenerate_css() returned early with no effect. The fallback deletes _elementor_css postmeta and the _elementor_global_css option so Elementor regenerates its compiled CSS on the first front-end visit.

Tests

22 tests / 85 assertions, all passing.
New: test_update_preserves_elementor_kit_byte_count — the regression test for this exact report. Inserts a realistic Kit payload (~6,387 bytes) into wp_postmeta, runs all 7 URL replacement passes through MUCD_Data::update(), then asserts:

  • All 4 system colours (#6EC1E4, #54595F, #7A7A7A, #61CE70) survive intact
  • All 4 typography entries (Roboto, Roboto Slab, Open Sans, Montserrat) survive intact
  • Logo and favicon URLs are rewritten to the clone
  • custom_css brand colours survive; hero background URL is rewritten
  • Byte count changes only by the predictable URL-length delta, not by a large unexpected amount
    Additional unit tests: test_get_primary_key_returns_correct_column, test_get_primary_key_is_cached, test_try_replace_elementor_kit_page_settings, test_try_replace_multiple_passes_preserve_data, test_try_replace_returns_original_on_unserialize_failure, test_try_replace_serialized_false_not_treated_as_error.

Files

  • EDIT: inc/duplication/data.php — primary key lookup + update fix + unserialize guard
  • EDIT: inc/compat/class-elementor-compat.php — DB fallback for CSS cache clear
  • EDIT: tests/WP_Ultimo/Duplication/MUCD_Data_Test.php — 8 new tests

Verification

vendor/bin/phpunit --no-coverage --filter MUCD_Data_Test
# Expected: OK (22 tests, 85 assertions)

Merged via PR #803 to main.
Merged by deterministic merge pass (pulse-wrapper.sh).


aidevops.sh v3.7.1 spent 5m on this as a headless bash routine.

@github-actions
Copy link
Copy Markdown

🔨 Build Complete - Ready for Testing!

📦 Download Build Artifact (Recommended)

Download the zip build, upload to WordPress and test:

🌐 Test in WordPress Playground (Very Experimental)

Click the link below to instantly test this PR in your browser - no installation needed!
Playground support for multisite is very limitied, hopefully it will get better in the future.

🚀 Launch in Playground

Login credentials: admin / password

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 12, 2026

Performance Test Results

Performance test results for 95baf22 are in 🛎️!

Note: the numbers in parentheses show the difference to the previous (baseline) test run. Differences below 2% or 0.5 in absolute values are not shown.

URL: /

Run DB Queries Memory Before Template Template WP Total LCP TTFB LCP - TTFB
0 40 37.73 MB 860.50 ms (+33.00 ms / +4% ) 156.00 ms 1040.50 ms 1976.00 ms 1889.95 ms 89.05 ms
1 56 49.02 MB 939.00 ms (+21.00 ms / +2% ) 147.50 ms (+4.50 ms / +3% ) 1085.50 ms 2050.00 ms 1971.80 ms 79.90 ms

@kenedytorcatt
Copy link
Copy Markdown
Contributor

Hi @superdav42 — URGENT follow-up: PR #803 fixed the Kit URL replacement, but in production at https://kursopro.com (~250+ subsites) we still get broken clones daily. Wrong colors, missing logos, broken menus, "TU LOGO AQUI" placeholders.

The postmeta layer for nav_menu_item, attachment, elementor_library, and the active Kit settings is still not copied by MUCD_Data::copy_data().

I've been running a 774-line mu-plugin patch in production since 2026-04-13 with zero failures. Filed full bug report + production-tested code + step-by-step integration instructions here:

Issue #820: #820

Once you integrate this into Multisite Ultimate natively, I can retire 2 mu-plugins from our stack. Happy to test any branch.

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

origin:interactive Created by interactive user session

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants